package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.Images;
import it.unibo.refolding.pico.PicoDataAnalyser.ContactPointAndSlope;
import it.unibo.refolding.pico.PicoDataAnalyser.ContactPointsAndSlope;
import it.unibo.refolding.pico.PicoDataAnalyser.PeakAnalysisInput;
import it.unibo.refolding.pico.RedundantPicoCurveInfo.PeakForceStrategy;
import it.unibo.refolding.pico.batch.AmnesticRetractionCurveBatch;
import it.unibo.refolding.pico.batch.NavigableMapRetractionCurveBatch;
import it.unibo.refolding.pico.batch.RetractionCurveBatch;
import it.unibo.refolding.pico.stripchart.AnalysisOptions;
import it.unibo.refolding.pico.stripchart.FetchRefoldBreakParams;
import it.unibo.refolding.pico.stripchart.JpHelper;
import it.unimi.dsi.fastutil.doubles.DoubleList;

import java.awt.Color;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.Parser;
import org.apache.commons.io.output.NullWriter;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.plexus.util.ExceptionUtils;
import org.xml.sax.SAXException;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

public class BatchApproachRetractionAnalysis {

  private static final Logger logger = Logger.getLogger(BatchApproachRetractionAnalysis.class.getSimpleName());

  private static final Pattern cpsPsiOrJunctionPointsPattern = Pattern.compile("^(.*)\\.(?:(?:cps)|(?:psi)|(?:junctionPoints))$");

  /**
   * Note that --clRangeStartNm=0 --clRangeEndNm=Infinity --wellSpacedAtEnd=1
   * --allowSkipped=0 only filter the svg and playlist elements, but not the
   * retraction curve file.
   * 
   * @throws InterruptedException
   */
  public static void main(String[] args) throws ParseException, IOException,
    XPathExpressionException, ParserConfigurationException, SAXException, InterruptedException {
    System.setProperty("java.awt.headless", "true");
    
    Options options = new Options();
    
    Option allowedTimeRelativeErrorOption = AnalysisOptions.createAllowedTimeRelativeErrorOption();
    options.addOption(allowedTimeRelativeErrorOption);

    Option contactPointAndSlopeDirOption = ContactPointAndSlopeHelper.createContactPointAndSlopeDirOption();
    options.addOption(contactPointAndSlopeDirOption);

    Option peakSlopesDirOption = PeakSlopesHelper.createPeakSlopesDirOption();
    options.addOption(peakSlopesDirOption);

    Option wildcardPatternOption = PicoUtil.createWildcardPatternOption();
    options.addOption(wildcardPatternOption);

    Option svgDefScaleOption = PicoUtil.createSvgDefScaleOption();
    options.addOption(svgDefScaleOption);
    
    Option svgIterationOffsetPnOption = PicoUtil.createSvgIterationOffsetPnOption();
    options.addOption(svgIterationOffsetPnOption);
    
    Option svgWildcardPatternOption = new Option(
      "svgWildcardPattern", true, "svgWildcardPattern for files");
    svgWildcardPatternOption.setRequired(false);
    options.addOption(svgWildcardPatternOption);

    Option dataDirOption = PicoUtil.createDataDirOption();
    options.addOption(dataDirOption);

    Option inputPathListOption = PicoUtil.createInputPathListOption();
    options.addOption(inputPathListOption);

    Option svgDirOption = AnalysisOptions.createSvgDirOption();
    options.addOption(svgDirOption);

    Option approachSkipDistanceNmOption = PicoUtil.createApproachSkipDistanceNmOption();
    options.addOption(approachSkipDistanceNmOption);

    Option defLsbVsZLsbSlopeOption = PicoUtil.createDefLsbVsZLsbSlopeOption();
    options.addOption(defLsbVsZLsbSlopeOption);

    Option minPeakForcePnOption = AnalysisOptions.createMinPeakForcePnOption();
    options.addOption(minPeakForcePnOption);

    Option minPeakContourLengthNmOption = AnalysisOptions.createMinPeakContourLengthNmOption();
    options.addOption(minPeakContourLengthNmOption);

    Option minContourLengthDeltaNmOption = AnalysisOptions.createMinContourLengthDeltaNmOption();
    options.addOption(minContourLengthDeltaNmOption);

    Option minPeakForceRelativeToEmpiricalBaselinePnOption = AnalysisOptions.createMinPeakForceRelativeToEmpiricalBaselinePnOption();
    options.addOption(minPeakForceRelativeToEmpiricalBaselinePnOption);

    Option medianAbsoluteDeviationLowerBoundPnOption = AnalysisOptions.createMedianAbsoluteDeviationLowerBoundPnOption();
    options.addOption(medianAbsoluteDeviationLowerBoundPnOption);

    Option leftMostlyHigherWindowSizeNmOption = AnalysisOptions.createLeftMostlyHigherWindowSizeNmOption();
    options.addOption(leftMostlyHigherWindowSizeNmOption);
    Option leftMostlyHigherFractionOption = AnalysisOptions.createLeftMostlyHigherFractionOption();
    options.addOption(leftMostlyHigherFractionOption);
    Option leftMostlyHigherPnOption = AnalysisOptions.createLeftMostlyHigherPnOption();
    options.addOption(leftMostlyHigherPnOption);

    Option rightMuchHigherThanAllPnOption = AnalysisOptions.createRightMuchHigherThanAllPnOption();
    options.addOption(rightMuchHigherThanAllPnOption);

    Option rightMuchHigherPnOption = AnalysisOptions.createRightMuchHigherPnOption();
    options.addOption(rightMuchHigherPnOption);

    Option atMostOnePeakWindowNmOption = AnalysisOptions.createAtMostOnePeakWindowNmOption();
    options.addOption(atMostOnePeakWindowNmOption);

    Option fileFormatOption = AnalysisOptions.createFileFormatOption();
    options.addOption(fileFormatOption);
    
    Option amnesticOption = new Option("amnestic", true, "");
    amnesticOption.setRequired(true);
    options.addOption(amnesticOption);
    
    Option iterationOption = new Option("iteration", true, "");
    iterationOption.setRequired(true);
    options.addOption(iterationOption);

    Option peakForceStrategyOption = createPeakForceStrategy();
    options.addOption(peakForceStrategyOption);

    Option fixedDeflectionSensitivityNmPerVOption = PicoUtil.createFixedDeflectionSensitivityNmPerVOption();
    options.addOption(fixedDeflectionSensitivityNmPerVOption);

    Option fixedSpringConstantNPerMOption = PicoUtil.createFixedSpringConstantNPerMOption();
    options.addOption(fixedSpringConstantNPerMOption);

    Option temperatureKOption = AnalysisOptions.createTemperatureKOption();
    options.addOption(temperatureKOption);

    Option persistenceLengthNmOption = AnalysisOptions.createPersistenceLengthNmOption();
    options.addOption(persistenceLengthNmOption);

    CommonClRefoldingOptions commonClRefoldingOptions = CommonClRefoldingOptions.create();
    commonClRefoldingOptions.addTo(options);

    Option wellSpacedAtEndOption = new Option(
      "wellSpacedAtEnd",
      true,
      "well spaced peaks at end including the detachment for which to create svg and hooke playlist entry");
    wellSpacedAtEndOption.setRequired(true);
    options.addOption(wellSpacedAtEndOption);

    Option allowSkippedOption = new Option(
      "allowSkipped", true, "how many skipped peaks to allow for");
    allowSkippedOption.setRequired(true);
    allowSkippedOption.setType(Long.TYPE);
    options.addOption(allowSkippedOption);

    Option retractionPlaylistOption = new Option(
      "retractionPlaylist", true, "name of retraction playlist file to be created");
    retractionPlaylistOption.setRequired(true);
    options.addOption(retractionPlaylistOption);

    Option filteredRetractionPlaylistOption = new Option(
      "filteredRetractionPlaylist", true, "name of filtered retraction playlist file to be created");
    filteredRetractionPlaylistOption.setRequired(true);
    options.addOption(filteredRetractionPlaylistOption);

    Option hookePlaylistOption = new Option(
      "hookePlaylist", true, "name of hooke playlist file to be created");
    hookePlaylistOption.setRequired(true);
    options.addOption(hookePlaylistOption);

    Option filteredHookePlaylistOption = new Option(
      "filteredHookePlaylist", true, "name of filtered hooke playlist");
    filteredHookePlaylistOption.setRequired(true);
    options.addOption(filteredHookePlaylistOption);

    Option infoPathOption = new Option("infoPath", true, "");
    infoPathOption.setRequired(true);
    options.addOption(infoPathOption);

    Option peakCsvPathOption = new Option(
      "peakCsvPath", true, "name of hooke playlist file to be created");
    peakCsvPathOption.setRequired(true);
    options.addOption(peakCsvPathOption);

    Option laxApproachContactPointOption = new Option(
      "laxApproachContactPoint", true,
      "When the retraction contact point is read from a .cps file, use also for approach.");
    laxApproachContactPointOption.setRequired(true);
    options.addOption(laxApproachContactPointOption);

    Option useApproachBaselineForRetractionOption = new Option(
      "useApproachBaselineForRetraction", true,
      "When the retraction contact point is read from a .cps file, use also for approach.");
    useApproachBaselineForRetractionOption.setRequired(true);
    options.addOption(useApproachBaselineForRetractionOption);

    Option resumeOption = new Option("resume", true, "boolean");
    resumeOption.setRequired(true);
    options.addOption(resumeOption);

    Option watchOption = new Option("watch", true, "boolean");
    watchOption.setRequired(true);
    options.addOption(watchOption);
    
    Option fixedSvgPathOption = new Option("fixedSvgPath", true, "");
    fixedSvgPathOption.setRequired(true);
    options.addOption(fixedSvgPathOption);

    // StripChart properties
    Option approachRampSizeNmOption = new Option("approachRampSizeNm", true, "");
    approachRampSizeNmOption.setRequired(false);
    options.addOption(approachRampSizeNmOption);

    Option approachVelNmPerSOption = new Option("approachVelNmPerS", true, "");
    approachVelNmPerSOption.setRequired(false);
    options.addOption(approachVelNmPerSOption);

    Option surfaceDelayMillisOption = new Option("surfaceDelayMillis", true, "");
    surfaceDelayMillisOption.setRequired(false);
    options.addOption(surfaceDelayMillisOption);

    Option retractionRampSizeNmOption = new Option("retractionRampSizeNm", true, "");
    retractionRampSizeNmOption.setRequired(false);
    options.addOption(retractionRampSizeNmOption);

    Option retractionVelNmPerSOption = new Option("retractionVelNmPerS", true, "");
    retractionVelNmPerSOption.setRequired(false);
    options.addOption(retractionVelNmPerSOption);

    Option threadsOption = new Option("threads", true, "");
    threadsOption.setRequired(true);
    threadsOption.setType(Integer.TYPE);
    options.addOption(threadsOption);

    Option junctionPointsDirOption = JpHelper.createJunctionPointsOption();
    options.addOption(junctionPointsDirOption);
    
    Option fetchRefoldBreakParamsOption = AnalysisOptions.createFetchRefoldBreakParamsOption();
    options.addOption(fetchRefoldBreakParamsOption);

    Option loopParamsOption = AnalysisOptions.createLoopParamsOption();
    options.addOption(loopParamsOption);
    
    Parser parser = new GnuParser();
    CommandLine line = parser.parse(options, args);
    
    final FetchRefoldBreakParams fetchRefoldBreakParams = AnalysisOptions.fetchRefoldBreakParams(
      line, fetchRefoldBreakParamsOption);
    final LoopParams loopParams = AnalysisOptions.loopParams(line, loopParamsOption);
    final DoubleList allowedTimeRelativeError = AnalysisOptions.allowedTimeRelativeError(
      line, allowedTimeRelativeErrorOption);
    final Path contactPointAndSlopeDir = ContactPointAndSlopeHelper.createCpsDirIfNeeded(
      contactPointAndSlopeDirOption, line);
    final Path peakSlopesDir = PeakSlopesHelper.createPeakSlopesDirIfNeeded(
      peakSlopesDirOption, line);
    String dataDir = line.getOptionValue(dataDirOption.getOpt());
    String inputPathList = line.getOptionValue(inputPathListOption.getOpt());
    Preconditions.checkArgument(
      StringUtils.isEmpty(dataDir) ^ StringUtils.isEmpty(inputPathList),
      "Must specify exactly one of dataDir and inputPathList.");
    final int iteration = Integer.parseInt(line.getOptionValue(iterationOption.getOpt()));
    final double svgDefScale = Double.parseDouble(line.getOptionValue(svgDefScaleOption.getOpt()));
    final double svgIterationOffsetN = Double.parseDouble(line.getOptionValue(svgIterationOffsetPnOption.getOpt())) / 1e12;
    String svgDir = line.getOptionValue(svgDirOption.getOpt());
    String wildcardPattern = line.getOptionValue(wildcardPatternOption.getOpt());
    String svgWildcardPattern = line.getOptionValue(svgWildcardPatternOption.getOpt());
    final double approachSkipDistanceM = Double.parseDouble(line.getOptionValue(approachSkipDistanceNmOption.getOpt())) / 1e9;
    String defLsbVsZLsbSlopeString = line.getOptionValue(defLsbVsZLsbSlopeOption.getOpt());
    final double slope = StringUtils.isEmpty(defLsbVsZLsbSlopeString) ? Double.NaN
      : Double.parseDouble(defLsbVsZLsbSlopeString);
    String fixedSpringConstantNPerMString = line.getOptionValue(fixedSpringConstantNPerMOption.getOpt());
    final double fixedSpringConstantNPerM = StringUtils.isEmpty(fixedSpringConstantNPerMString) ? Double.NaN
      : Double.parseDouble(fixedSpringConstantNPerMString);
    String fixedDeflectionSensitivityNmPerVString = line.getOptionValue(fixedDeflectionSensitivityNmPerVOption.getOpt());
    final double fixedDeflectionSensitivityMPerV = StringUtils.isEmpty(fixedDeflectionSensitivityNmPerVString) ? Double.NaN
      : Double.parseDouble(fixedDeflectionSensitivityNmPerVString) / 1e9;
    final Function<? super Double, Double> minimumPeakForceN = AnalysisOptions.minPeakForceN(
      line, minPeakForcePnOption);
    final double minimumPeakContourLengthM = AnalysisOptions.minPeakContourLengthM(
      line, minPeakContourLengthNmOption);
    final double minContourLengthDeltaM = AnalysisOptions.minContourLengthDeltaM(
      line, minContourLengthDeltaNmOption);
    final double minPeakForceRelativeToEmpiricalBaselineN = AnalysisOptions.minPeakForceRelativeToEmpiricalBaselineN(
      line, minPeakForceRelativeToEmpiricalBaselinePnOption);
    final Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN = AnalysisOptions.medianAbsoluteDeviationLowerBoundN(
      line, medianAbsoluteDeviationLowerBoundPnOption);
    final double leftMostlyHigherWindowSizeM = AnalysisOptions.leftMostlyHigherWindowSizeM(
      line, leftMostlyHigherWindowSizeNmOption);
    final double leftMostlyHigherFraction = AnalysisOptions.leftMostlyHigherFraction(
      line, leftMostlyHigherFractionOption);
    final double leftMostlyHigherN = AnalysisOptions.leftMostlyHigherN(
      line, leftMostlyHigherPnOption);
    final Function<? super Double, Double> rightMuchHigherThanAllN = AnalysisOptions.rightMuchHigherThanAllN(
      line, rightMuchHigherThanAllPnOption);
    final Function<? super Double, Double> rightMuchHigherN = AnalysisOptions.rightMuchHigherN(
      line, rightMuchHigherPnOption);
    final double atMostOnePeakWindowM = AnalysisOptions.atMostOnePeakWindowM(
      line, atMostOnePeakWindowNmOption);
    final double temperatureK = AnalysisOptions.temperatureK(line, temperatureKOption);
    final double persistenceLengthM = AnalysisOptions.persistenceLengthM(
      line, persistenceLengthNmOption);
    final FileFormat fileFormat = FileFormat.valueOf(line.getOptionValue(
      fileFormatOption.getOpt(), FileFormat.NANOSCOPE.name()).toUpperCase(Locale.ROOT));

    final PeakForceStrategy peakForceStrategy = PeakForceStrategy.valueOf(line.getOptionValue(
      peakForceStrategyOption.getOpt()).toUpperCase(Locale.ROOT));

    final double clRangeStartM = commonClRefoldingOptions.clRangeStartM(line);
    final double clRangeEndM = commonClRefoldingOptions.clRangeEndM(line);
    final int wellSpacedAtEnd = Integer.parseInt(line.getOptionValue(wellSpacedAtEndOption.getOpt()));
    final int allowSkipped = Integer.parseInt(line.getOptionValue(allowSkippedOption.getOpt()));

    String retractionPlaylistPathString = line.getOptionValue(retractionPlaylistOption.getOpt());
    String filteredRetractionPlaylistPathString = line.getOptionValue(filteredRetractionPlaylistOption.getOpt());
    String hookePlaylistPathString = line.getOptionValue(hookePlaylistOption.getOpt());
    String filteredHookePlaylistPathString = line.getOptionValue(filteredHookePlaylistOption.getOpt());
    String infoPath = line.getOptionValue(infoPathOption.getOpt());
    String peakCsvPathString = line.getOptionValue(peakCsvPathOption.getOpt());
    String fixedSvgPath = line.getOptionValue(fixedSvgPathOption.getOpt());
    final boolean amnestic = parseBoolean(amnesticOption, line);
    final boolean resume = parseBoolean(resumeOption, line);
    final boolean laxApproachContactPoint = parseBoolean(laxApproachContactPointOption, line);
    final boolean useApproachBaselineForRetraction = parseBoolean(useApproachBaselineForRetractionOption, line);
    boolean watch = parseBoolean(watchOption, line);
    int threads = Integer.parseInt(line.getOptionValue(threadsOption.getOpt()));

    final Path junctionPointsDir = JpHelper.createJpDirIfNeeded(junctionPointsDirOption, line);

    final double kbtOverP = Wlc.KB * temperatureK / persistenceLengthM;
    final RetractionCurveBatch batch = amnestic ? new AmnesticRetractionCurveBatch(
      iteration, Paths.get(retractionPlaylistPathString), Paths.get(peakCsvPathString),
      Paths.get(hookePlaylistPathString), clRangeStartM, clRangeEndM, wellSpacedAtEnd,
      allowSkipped, Paths.get(filteredRetractionPlaylistPathString),
      Paths.get(filteredHookePlaylistPathString)) : new NavigableMapRetractionCurveBatch(
      iteration, Paths.get(retractionPlaylistPathString), Paths.get(peakCsvPathString),
      Paths.get(hookePlaylistPathString), clRangeStartM, clRangeEndM, wellSpacedAtEnd,
      allowSkipped, Paths.get(filteredRetractionPlaylistPathString),
      Paths.get(filteredHookePlaylistPathString),
      new ConcurrentSkipListMap<Path, RetractionCurve>());
    if (resume) {
      batch.load();
    }
    logger.log(Level.INFO, "Reusing {0} entries.", batch.size());
    final Path svgDirectory = StringUtils.isEmpty(svgDir) ? null
      : CommonThings.createDirIfNeeded(svgDir);
    final PathMatcher svgPathMatcher = (StringUtils.isEmpty(inputPathList) ? Paths.get(dataDir)
      : Paths.get(inputPathList)).getFileSystem().getPathMatcher(svgWildcardPattern);
    Path fixedSvgFilePath = StringUtils.isEmpty(fixedSvgPath) ? null : Paths.get(fixedSvgPath);
    ExecutorService executor = threads == 0 ? callerRunsExecutor() : new ThreadPoolExecutor(
      threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(),
      new ThreadPoolExecutor.CallerRunsPolicy());
    List<Path> files = PicoUtil.computeFileList(dataDir, inputPathList, wildcardPattern);
    final int fileCount = files.size();
    logger.log(
      Level.INFO,
      "Found at most {0} files with dataDir={1}, inputPathList={2} and wildcardPattern={3}.",
      new Object[] {fileCount, dataDir, inputPathList, wildcardPattern});
    final long startTime = System.nanoTime();
    final AtomicInteger svgCount = new AtomicInteger();
    try (Writer infoWriter = StringUtils.isEmpty(infoPath) ? NullWriter.NULL_WRITER
      : Files.newBufferedWriter(Paths.get(infoPath), Charsets.ISO_8859_1)) {
      for (int i = 0; i < files.size(); ++i) {
        final Path file = files.get(i);
        executor.execute(new Runnable() {
          @Override
          public void run() {
            try {
              runWithInputOutput();
            } catch (IOException ex) {
              logger.log(Level.WARNING, "Message: {0}    Stack trace: {1}", new Object[] {
              ex, Arrays.toString(ex.getStackTrace())});
            } catch (IllegalArgumentException ex) {
              /*
               * IllegalArgumentException for
               * RetractionPeak.<init>(RetractionPeak.java:43) when stripchart
               * *lose files are not correctly parsed.
               */
              logger.log(Level.SEVERE, "Message: {0}    Stack trace: {1}", new Object[] {
              ex, Arrays.toString(ex.getStackTrace())});
            }
          }

          public void runWithInputOutput() throws IOException {
            int previousApproxSize = batch.size();
            if (Files.isRegularFile(file)) {
              // We only keep the last retraction curve.
              RetractionCurve retractionCurve;
              Path svgPath = svgDirectory == null ? null : svgDirectory.resolve(file.getFileName()
                + ".svg");
              if (!resume || !batch.containsKey(file)) {
                Parsed parsed = PicoUtil.fix(
                  PicoUtil.parse(file, fileFormat, fetchRefoldBreakParams, loopParams, allowedTimeRelativeError, junctionPointsDir),
                  fixedDeflectionSensitivityMPerV, fixedSpringConstantNPerM);
                if (parsed.picoDataList.isEmpty()) {
                  retractionCurve = RetractionCurve.empty(file.toString());
                } else {
                  for (int i = 0; i < parsed.picoDataList.size(); ++i) {
                    PlainPicoData plainPicoData = parsed.picoDataList.get(i);
                    Double mad = medianAbsoluteDeviationLowerBoundN.apply(plainPicoData.retraction.velocity);
                    Double rmh = rightMuchHigherN.apply(plainPicoData.retraction.velocity);
                    Double mpf = minimumPeakForceN.apply(plainPicoData.retraction.velocity);
                    // Optimisation.
                    if (infoWriter != NullWriter.NULL_WRITER) {
                      String info = String.format(
                        Locale.ROOT,
                        "RampSizeNm=%.3f ZScanStartNm=%.3f ApproachVelNmPerS=%.3f ApproachSamples=%d SurfaceDelayMs=%.3f RetractionVelNmPerS=%.3f RetractionSamples=%d DeflectionLimitV=%s DeflectionSensitivityNmPerV=%s SpringConstantNPerM=%f MedianAbsoluteDeviationLowerBoundPn=%s RightMuchHigherPn=%s MinimumPeakForcePn=%s Filename=%s Index=%d\n",
                        plainPicoData.rampSizeMetres() * 1e9, plainPicoData.zScanStartMetres() * 1e9,
                        plainPicoData.approach.velocity * 1e9, plainPicoData.approach.timeInstantCount(),
                        plainPicoData.rampDelayS * 1e3, plainPicoData.retraction.velocity * 1e9,
                        plainPicoData.retraction.timeInstantCount(), plainPicoData.deflectionLimitV,
                        plainPicoData.deflectionSensitivityMPerV * 1e9,
                        plainPicoData.springConstantNPerM, mad * 1e12, rmh * 1e12, mpf * 1e12,
                        file.getFileName(), i);
                      synchronized (infoWriter) {
                        infoWriter.write(info);
                      }
                    }
                  }
                  ContactPointsAndSlope cps = cps(
                    file, parsed.picoDataList.get(iteration), approachSkipDistanceM,
                    laxApproachContactPoint, useApproachBaselineForRetraction, slope, contactPointAndSlopeDir);
                  Path peakSlopesPath = peakSlopesDir == null ? null
                    : PeakSlopesHelper.peakSlopesFile(peakSlopesDir, file.getFileName().toString());
                  List<PeakSlopes> peakSlopesList = (peakSlopesPath != null & Files.isRegularFile(peakSlopesPath)) ? PeakSlopesHelper.extractPeakSlopesList(peakSlopesPath)
                    : null;
                  List<RedundantPicoCurveInfo> a = analyseWithCpsAndOptionalPeakSlopes(
                    minimumPeakForceN, minimumPeakContourLengthM, minContourLengthDeltaM,
                    minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
                    leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN, rightMuchHigherThanAllN, rightMuchHigherN,
                    atMostOnePeakWindowM, kbtOverP, file, parsed.picoDataList, cps, peakSlopesList);
                  // FetchRefoldBreak only writes the first segment.
                  Preconditions.checkState(peakSlopesList == null || peakSlopesList.size() <= a.size());
                  if ((peakSlopesList == null || peakSlopesList.size() != a.size()) & peakSlopesPath != null) {
                    writePeakSlopes(a, peakSlopesPath);
                  }
                  retractionCurve = toRetractionCurve(
                    file, CommonThings.last(parsed.picoDataList).springConstantNPerM, a,
                    peakForceStrategy);
                  boolean toInclude = svgPathMatcher.matches(file)
                    & (retractionCurve.size() > iteration && PicoDataAnalyser.wellSpacedAtEnd(
                      retractionCurve.peakContourLengthsFromContactPointOnwards(iteration),
                      clRangeStartM, clRangeEndM, allowSkipped) >= wellSpacedAtEnd
                      & a.get(iteration) != null);
                  if (svgPath != null & toInclude) {
                    Map<Color, List<Shape>> shapes = draw(parsed, a, svgIterationOffsetN, svgDefScale);
                    Images.saveSvg(shapes, svgPath);
                    svgCount.incrementAndGet();
                  }
                }
                assert retractionCurve != null;
                RetractionCurve old = batch.put(file, retractionCurve);
                Preconditions.checkState(old == null);
              } else {
                retractionCurve = batch.get(file);
              }
              boolean toInclude = svgPathMatcher.matches(file)
                & (retractionCurve.size() > iteration && PicoDataAnalyser.wellSpacedAtEnd(
                  retractionCurve.peakContourLengthsFromContactPointOnwards(iteration),
                  clRangeStartM, clRangeEndM, allowSkipped) >= wellSpacedAtEnd);
              if (svgPath != null & !toInclude) {
                Files.deleteIfExists(svgPath);
              }
            } else {
              logger.log(Level.WARNING, "Skipping {0} because it is not a regular file.", file);
            }
            // Note that we may skip some size values.
            int approxSize = batch.size();
            double percentage = approxSize * 100.0 / fileCount;
            double previousPercentage = previousApproxSize * 100.0 / fileCount;
            if (Math.ceil(percentage) != Math.ceil(previousPercentage)) {
              // We may log between 0 and "threads" times inclusive the same
              // value.
              long now = System.nanoTime();
              double seconds = (now - startTime) / 1e9;
              logger.log(Level.INFO, String.format(
                Locale.ROOT,
                "Processed: %d/%d (%d%%)  Images: %d  Time: %.2f s (%.2f/s)  ETA: %.2f s",
                approxSize, fileCount, (int) Math.ceil(percentage), svgCount.get(), seconds,
                approxSize / seconds, (fileCount - approxSize) * seconds / approxSize));
            }
          }

        });
      }
      executor.shutdown();
      executor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
      batch.write();
    }
    if (watch) {
      WatchService watcher = FileSystems.getDefault().newWatchService();
      for (Path dir : new Path[] {junctionPointsDir, contactPointAndSlopeDir, peakSlopesDir}) {
        if (dir != null) {
          dir.register(
            watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW);
        }
      }
      while (true) {
        WatchKey key = watcher.take();
        List<Path> changed = new ArrayList<>();
        for (WatchEvent<?> event : key.pollEvents()) {
          Preconditions.checkState(event.count() >= 1);
          Kind<?> kind = event.kind();
          if (kind == StandardWatchEventKinds.OVERFLOW) {
            changed.clear();
            changed.addAll(batch.keySet());
            break;
          }
          Preconditions.checkState(kind == StandardWatchEventKinds.ENTRY_MODIFY);
          Path name = (Path) event.context();
          Matcher m = cpsPsiOrJunctionPointsPattern.matcher(name.toString());
          Preconditions.checkState(m.matches());
          String filename = m.group(1);
          Path path = null;
          for (Map.Entry<Path, RetractionCurve> entry : batch.entrySet()) {
            if (entry.getKey().getFileName().toString().equals(filename)) {
              Preconditions.checkState(path == null, "%s already added.", path);
              path = entry.getKey();
            }
          }
          Preconditions.checkState(path != null);
          changed.add(path);
        }
        logger.log(Level.INFO, "Stale: {0}", changed);
        for (Path file : changed) {
          Parsed parsed = PicoUtil.fix(
            PicoUtil.parse(file, fileFormat, fetchRefoldBreakParams, loopParams, allowedTimeRelativeError, junctionPointsDir),
            fixedDeflectionSensitivityMPerV, fixedSpringConstantNPerM);
          RetractionCurve retractionCurve;
          if (parsed.picoDataList.isEmpty()) {
            retractionCurve = RetractionCurve.empty(file.toString());
          } else {
            ContactPointsAndSlope cps = cps(
              file, parsed.picoDataList.get(iteration), approachSkipDistanceM,
              laxApproachContactPoint, useApproachBaselineForRetraction, slope, contactPointAndSlopeDir);
            Path peakSlopesPath = peakSlopesDir == null ? null : PeakSlopesHelper.peakSlopesFile(
              peakSlopesDir, file.getFileName().toString());
            List<PeakSlopes> peakSlopesList = (peakSlopesPath != null & Files.isRegularFile(peakSlopesPath)) ? PeakSlopesHelper.extractPeakSlopesList(peakSlopesPath)
              : null;
            List<RedundantPicoCurveInfo> a = analyseWithCpsAndOptionalPeakSlopes(
              minimumPeakForceN, minimumPeakContourLengthM, minContourLengthDeltaM,
              minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
              leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
              rightMuchHigherThanAllN, rightMuchHigherN,
              atMostOnePeakWindowM, kbtOverP, file, parsed.picoDataList, cps, peakSlopesList);
            // FetchRefoldBreak only writes the first segment.
            Preconditions.checkState(peakSlopesList == null || peakSlopesList.size() <= a.size());
            if ((peakSlopesList == null || peakSlopesList.size() != a.size()) & peakSlopesPath != null) {
              writePeakSlopes(a, peakSlopesPath);
            }
            retractionCurve = toRetractionCurve(
              file, CommonThings.last(parsed.picoDataList).springConstantNPerM, a, peakForceStrategy);
            assert retractionCurve.size() > iteration : file;
            boolean toInclude = svgPathMatcher.matches(file)
              & PicoDataAnalyser.wellSpacedAtEnd(
                retractionCurve.peakContourLengthsFromContactPointOnwards(iteration),
                clRangeStartM, clRangeEndM, allowSkipped) >= wellSpacedAtEnd
              & a.get(iteration) != null;

            final Map<Color, List<Shape>> color2shapes;
            if ((svgDirectory != null & toInclude) | fixedSvgFilePath != null) {
              color2shapes = draw(parsed, a, svgIterationOffsetN, svgDefScale);
            } else {
              color2shapes = null;
            }
            String svgFilename = file.getFileName().toString() + ".svg";
            if (svgDirectory != null) {
              Path svgPath = svgDirectory.resolve(svgFilename);
              if (toInclude) {
                logger.log(Level.INFO, "Writing svg file: {0}", svgPath);
                Images.saveSvg(color2shapes, svgPath);
                if (fixedSvgFilePath != null) {
                  copySvgFile(svgPath, fixedSvgFilePath);
                }
              } else {
                boolean deleted = Files.deleteIfExists(svgPath);
                if (deleted) {
                  logger.log(Level.INFO, "Deleted file: {0}", svgPath);
                }
                if (fixedSvgFilePath != null) {
                  logger.log(Level.INFO, "Writing svg file: {0}", fixedSvgFilePath);
                  Images.saveSvg(color2shapes, fixedSvgFilePath);
                }
              }
            } else {
              if (fixedSvgFilePath != null) {
                logger.log(Level.INFO, "Writing svg file: {0}", fixedSvgFilePath);
                Images.saveSvg(color2shapes, fixedSvgFilePath);
              }
            }
            assert retractionCurve != null;
            batch.put(file, retractionCurve);
          }
        }
        batch.write();
        boolean valid = key.reset();
        Preconditions.checkState(valid);
      }
    }
  }

  private static ExecutorService callerRunsExecutor() {
    return new ExecutorService() {

      @Override
      public void execute(Runnable command) {
        command.run();
      }

      @Override
      public void shutdown() {
        // Nothing to do.
      }

      @Override
      public List<Runnable> shutdownNow() {
        return null;
      }

      @Override
      public boolean isShutdown() {
        return false;
      }

      @Override
      public boolean isTerminated() {
        return false;
      }

      @Override
      public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        // Nothing to do.
        return true;
      }

      @Override
      public <T> Future<T> submit(Callable<T> task) {
        return null;
      }

      @Override
      public <T> Future<T> submit(Runnable task, T result) {
        return null;
      }

      @Override
      public Future<?> submit(Runnable task) {
        return null;
      }

      @Override
      public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
        return null;
      }

      @Override
      public <T> List<Future<T>> invokeAll(
        Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
        throws InterruptedException {
        return null;
      }

      @Override
      public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException,
        ExecutionException {
        return null;
      }

      @Override
      public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        return null;
      }
    };
  }

  public static Boolean parseBoolean(Option amnesticOption, CommandLine line) {
    return ImmutableMap.of("true", true, "false", false).get(
      line.getOptionValue(amnesticOption.getOpt()).toLowerCase(Locale.ROOT));
  }

  public static Map<Color, List<Shape>> draw(
    Parsed p, List<RedundantPicoCurveInfo> a, double svgIterationOffsetN, double svgDefScale) {
    AffineTransform[] ats = new AffineTransform[a.size()];
    for (int i = 0; i < a.size(); ++i) {
      ats[i] = AffineTransform.getTranslateInstance(0, i * svgIterationOffsetN
        / p.picoDataList.get(i).deflectionLsbToNewtonsFactor() * svgDefScale);
    }
    List<Path2D.Float> paths = PicoShapes.createPaths(
      p.z, p.def, p.jp, svgDefScale, (short) 0, (short) 0);
    int k = a.size() * (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1);
    List<Color> segmentColors = PicoShapes.colorsForPieces(1 + k);
    List<Map<Color, List<Shape>>> maps = new ArrayList<>();
    for (int i = 0; i < paths.size(); ++i) {
      Path2D.Float path = paths.get(i);
      Color c = segmentColors.get(i % segmentColors.size());
      final List<Shape> ts;
      if (i == 0) {
        ts = ImmutableList.<Shape>of(path);
      } else {
        ts = createTransformedShape(ats[(i - 1) / (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1)], ImmutableList.<Shape>of(path));
      }
      maps.add(ImmutableMap.of(c, ts));
    }
    int lastAnalysisIndex = -1;
    for (int i = 0; i < p.picoDataList.size(); ++i) {
      RedundantPicoCurveInfo info = a.get(i);
      if (info != null) {
        lastAnalysisIndex = i;
        List<Shape> approachLines = PicoShapes.drawApproachContactPointAndSlope(
          info.approachContactPointAndSlope, info.plainPicoData.approach,
          info.plainPicoData.zLsbToMetresFactor, svgDefScale);
        Color approachColor = segmentColors.get(1 + i * (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1));
        maps.add(ImmutableMap.of(approachColor, createTransformedShape(ats[i], approachLines)));
        
        Color retractionColor = segmentColors.get(1 + i * (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1) + 2);
        
        List<Shape> wlcShapes = PicoShapes.drawWlc(
          info.plainPicoData, info.retractionContactPoint, info.offsetAndDeflections,
          info.approachContactPointAndSlope.slopeZLsbDefLsb,
          info.peakOrNots, info.wlcPeakForces(), info.kbtOverP, svgDefScale);
        maps.add(ImmutableMap.of(retractionColor, createTransformedShape(ats[i], wlcShapes)));
        
        List<Shape> interpeaks = PicoShapes.drawInterpeaks(
          info.plainPicoData, info.offsetAndDeflections, info.peakOrNots, svgDefScale);
        maps.add(ImmutableMap.of(retractionColor, createTransformedShape(ats[i], interpeaks)));
        
        List<Shape> redPeakRelatedShapes = PicoShapes.drawPeakShapes(
          info.plainPicoData, info.offsetAndDeflections, info.peakOrNots, svgDefScale);
        maps.add(ImmutableMap.of(retractionColor, createTransformedShape(ats[i], redPeakRelatedShapes)));
        
        if (svgIterationOffsetN != 0) {
          List<Shape> retractionShapes = PicoShapes.drawRetractionInfo(
            info.plainPicoData, info.retractionContactPoint,
            info.approachContactPointAndSlope.slopeZLsbDefLsb, svgDefScale);
          Color lastColor = segmentColors.get(1 + i * (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1)
            + 2);
          maps.add(ImmutableMap.of(lastColor, createTransformedShape(ats[i], retractionShapes)));
        }
      }
    }
    if (svgIterationOffsetN == 0) {
      if (lastAnalysisIndex != -1) {
        RedundantPicoCurveInfo lastPicoAnalysis = a.get(lastAnalysisIndex);
        List<Shape> retractionShapes = PicoShapes.drawRetractionInfo(
          lastPicoAnalysis.plainPicoData, lastPicoAnalysis.retractionContactPoint,
          lastPicoAnalysis.approachContactPointAndSlope.slopeZLsbDefLsb, svgDefScale);
        Color lastColor = segmentColors.get(1 + lastAnalysisIndex * (ApproachRetractionLoop.APPROACH_RETRACTION_SEGMENTS + 1)
          + 2);
        maps.add(ImmutableMap.of(lastColor, createTransformedShape(ats[lastAnalysisIndex], retractionShapes)));
      }
    }
    return CommonThings.unionMapsOfLists(maps);
  }

  private static List<Shape> createTransformedShape(
    AffineTransform affineTransform, List<Shape> shapes) {
    List<Shape> t = new ArrayList<>(shapes.size());
    for (Shape s : shapes) {
      t.add(affineTransform.createTransformedShape(s));
    }
    return t;
  }

  public static Option createPeakForceStrategy() {
    Option peakForceStrategyOption = new Option("peakForceStrategy", true, "raw, wlc or min");
    peakForceStrategyOption.setRequired(true);
    return peakForceStrategyOption;
  }

  static void writePeakSlopes(List<RedundantPicoCurveInfo> a, Path peakSlopesPath)
    throws IOException {
    if (peakSlopesPath != null) {
      List<PeakSlopes> b = RedundantPicoCurveInfo.peakSlopes(a);
      PeakSlopesHelper.writeToFile(b, peakSlopesPath);
    }
  }

  private static RetractionCurve toRetractionCurve(
    Path file, double springConstantNPerM, List<RedundantPicoCurveInfo> a,
    PeakForceStrategy peakForceStrategy) {
    List<RetractionSegment> segments = new ArrayList<>();
    for (RedundantPicoCurveInfo info : a) {
      segments.add(toRetractionSegment(info, peakForceStrategy));
    }
    return new RetractionCurve(file.toString(), springConstantNPerM, segments);
  }

  public static List<RedundantPicoCurveInfo> analyseWithCpsAndOptionalPeakSlopes(
    Function<? super Double, Double> minimumPeakForceN, double minimumPeakContourLengthM,
    double minContourLengthDeltaM, double minPeakForceRelativeToEmpiricalBaselineN,
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    Function<? super Double, Double> rightMuchHigherThanAllN,
    Function<? super Double, Double> rightMuchHigherN, double atMostOnePeakWindowM,
    double kbtOverP, Path file, List<PlainPicoData> plainPicoDataList, ContactPointsAndSlope cps,
    List<PeakSlopes> peakSlopesList) {
    if (peakSlopesList != null) {
      /*
       * FetchRefoldBreak only analyses fetch. Differently from PeakSlopesMain,
       * here we fill with null so that the peaks are computed for the refold
       * and successive phases.
       */
      peakSlopesList = PeakSlopesHelper.fillUpPeakSlopes(plainPicoDataList, null, peakSlopesList);
    }
    List<RedundantPicoCurveInfo> a = new ArrayList<>();
    for (int i = 0; i < plainPicoDataList.size(); ++i) {
      PeakSlopes peakSlopes = peakSlopesList == null ? null : peakSlopesList.get(i);
      PlainPicoData plainPicoData = plainPicoDataList.get(i);
      RedundantPicoCurveInfo info = analyseOrNull(
        file, plainPicoData, minimumPeakForceN.apply(plainPicoData.retraction.velocity),
        minimumPeakContourLengthM, minContourLengthDeltaM,
        minPeakForceRelativeToEmpiricalBaselineN,
        medianAbsoluteDeviationLowerBoundN.apply(plainPicoData.retraction.velocity),
        leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
        rightMuchHigherThanAllN.apply(plainPicoData.retraction.velocity),
        rightMuchHigherN.apply(plainPicoData.retraction.velocity), atMostOnePeakWindowM, kbtOverP,
        cps, peakSlopes);
      a.add(info);
    }
    return a;
  }

  static void copySvgFile(Path svgFile, Path fixedSvgFilePath) throws IOException {
    logger.log(Level.INFO, "Copy from {0} to {1}.", new Object[] {svgFile, fixedSvgFilePath});
    Files.copy(svgFile, fixedSvgFilePath, StandardCopyOption.REPLACE_EXISTING);
  }

  static RetractionSegment toRetractionSegment(
    RedundantPicoCurveInfo info, PeakForceStrategy peakForceStrategy) {
    RetractionSegment retractionSegment = info != null ? info.retractionSegment(peakForceStrategy)
      : RetractionSegment.EMPTY;
    logger.log(Level.FINE, "{0}", retractionSegment);
    return retractionSegment;
  }

  static RedundantPicoCurveInfo analyseOrNull(
    Path file, PlainPicoData plainPicoData, double minimumPeakForceN,
    double minimumPeakContourLengthM, double minContourLengthDeltaM,
    double minPeakForceRelativeToEmpiricalBaselineN, double medianAbsoluteDeviationLowerBoundN,
    double leftMostlyHigherWindowSizeM, double leftMostlyHigherFraction, double leftMostlyHigherN,
    double rightMuchHigherThanAllN, double rightMuchHigherN,
    double atMostOnePeakWindowM, double kbtOverP, ContactPointsAndSlope cps, PeakSlopes peakSlopes) {
    if (cps == null) {
      return null;
    }
    PeakAnalysisInput input = new PeakAnalysisInput(
      plainPicoData, cps, minimumPeakForceN, minimumPeakContourLengthM, minContourLengthDeltaM,
      minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
      leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN, rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM,
      kbtOverP);
    if (input.plainPicoData.sharedRetraction().timeInstantCount() == 0) {
      // Probably an empty .csv file.
      return null;
    }
    try {
      return PicoDataAnalyser.analyseWithGivenContactPointsAndSlope(input, peakSlopes);
    } catch (RuntimeException ex) {
      logger.log(Level.WARNING, "Exception {0} with stacktrace {1} analysing {2}.", new Object[] {
      ex, ExceptionUtils.getStackTrace(ex), file});
      return null;
    }
  }

  private static ContactPointsAndSlope cps(
    Path file, PlainPicoData plainPicoData, double approachSkipDistanceM,
    boolean laxApproachContactPoint, boolean useApproachBaselineForRetraction, double fixedSlope, Path contactPointAndSlopeDir) {
    try {
      return BatchApproachRetractionAnalysis.readOrComputeAndWriteContactPointAndSlopeOrNull(
        plainPicoData, approachSkipDistanceM, laxApproachContactPoint, useApproachBaselineForRetraction, fixedSlope,
        contactPointAndSlopeDir, file.getFileName().toString());
    } catch (IOException | RuntimeException ex) {
      logger.log(Level.WARNING, "Exception {0} analysing {1}.", new Object[] {ex, file});
      return null;
    }
  }

  /**
   * @param plainPicoData
   * @param approachSkipDistanceM
   *          How much to skip from the start of the approach phase when
   *          computing the slope and approach contact point. That's to fix
   *          electronics issue that introduces some kind of decay of an initial
   *          deflection value that depends on the traversed distance and not on
   *          time.
   * @param fixedSlope
   * @param useApproachBaselineForRetraction
   * @param filename
   * @param contactPointAndSlopeDir
   * @return
   * @throws IOException
   */
  public static ContactPointsAndSlope readOrComputeAndWriteContactPointAndSlope(
    PlainPicoData plainPicoData, double approachSkipDistanceM, double fixedSlope,
    boolean laxApproachContactPoint, boolean useApproachBaselineForRetraction, String filename, Path contactPointAndSlopeDir)
    throws IOException {
    final ContactPointsAndSlope contactPointsAndSlope;
    if (contactPointAndSlopeDir == null) {
      ContactPointAndSlope approachContactPointAndSlope = findApproachContactPointAndSlope(
        plainPicoData, approachSkipDistanceM, fixedSlope);
      contactPointsAndSlope = PicoDataAnalyser.findContactPointsAndSlope(
        plainPicoData, approachContactPointAndSlope, useApproachBaselineForRetraction);
    } else {
      Path contactPointAndSlopeFile = ContactPointAndSlopeHelper.contactPointAndSlopeFile(
        contactPointAndSlopeDir, filename);
      if (Files.isRegularFile(contactPointAndSlopeFile)) {
        ContactPointAndSlope retractionContactPointAndSlope = ContactPointAndSlopeHelper.extractContactPointAndSlope(contactPointAndSlopeFile);
        /*
         * If retraction slope is known, use it also for approach when
         * laxContactPoint is true.
         */
        ContactPointAndSlope approachContactPointAndSlope = laxApproachContactPoint ? retractionContactPointAndSlope
          : findApproachContactPointAndSlope(
            plainPicoData, approachSkipDistanceM, retractionContactPointAndSlope.slopeZLsbDefLsb);
        contactPointsAndSlope = new ContactPointsAndSlope(
          approachContactPointAndSlope.contactPoint(),
          retractionContactPointAndSlope.contactPoint(),
          retractionContactPointAndSlope.slopeZLsbDefLsb);
      } else {
        ContactPointAndSlope approachContactPointAndSlope = findApproachContactPointAndSlope(
          plainPicoData, approachSkipDistanceM, fixedSlope);
        contactPointsAndSlope = PicoDataAnalyser.findContactPointsAndSlope(
          plainPicoData, approachContactPointAndSlope, useApproachBaselineForRetraction);
        ContactPointAndSlopeHelper.writeToFile(
          contactPointsAndSlope.retractionContactPointAndSlope(), contactPointAndSlopeFile);
      }
    }
    return contactPointsAndSlope;
  }

  static ContactPointAndSlope findApproachContactPointAndSlope(
    PlainPicoData plainPicoData, double approachSkipDistanceM, double fixedSlope) {
    ContactPointAndSlope approachContactPointAndSlope = PicoDataAnalyser.findApproachContactPointAndSlope(
      plainPicoData.sharedApproach().subRange(
        0,
        1 + PicoDataAnalyser.offsetOfStartingContiguousRegionNotBefore(
          plainPicoData.sharedApproach().z, approachSkipDistanceM
            / plainPicoData.zLsbToMetresFactor)), fixedSlope);
    return approachContactPointAndSlope;
  }

  static ContactPointsAndSlope readOrComputeAndWriteContactPointAndSlopeOrNull(
    PlainPicoData fetch, double approachSkipDistanceM, boolean laxApproachContactPoint,
    boolean useApproachBaselineForRetraction, double fixedSlope, Path contactPointAndSlopeDir, String filename) throws IOException {
    try {
      return readOrComputeAndWriteContactPointAndSlope(
        fetch, approachSkipDistanceM, fixedSlope, laxApproachContactPoint, useApproachBaselineForRetraction, filename,
        contactPointAndSlopeDir);
    } catch (IllegalArgumentException ex) {
      logger.log(
        Level.WARNING, "Could not analyse {0} because of {1}.", new Object[] {filename, ex});
      return null;
    } catch (IllegalStateException ex) {
      logger.log(
        Level.WARNING, "Could not analyse {0} because of {1}.", new Object[] {filename, ex});
      return null;
    }
  }
}
